4. Initiating Object Motion
You should now feel comfortable with reading user
input from the touch screen. Before we finish examining screen input,
let's discuss a couple of common movement patterns that you might want
to include in your games: dragging and flicking.
4.1. Dragging Objects
The code required to drag objects is very
straightforward. If a touch point is held, we simply need to find the
movement distance between its current and previous locations and add it
to the position of the objects that are being dragged.
The first part of dragging some objects is to allow
them to be selected. Sometimes the selection will be a separate input
from the drag (where the objects are tapped and then dragged afterward),
but in most cases a drag will include object selection when contact
with the screen is first established.
This is easy to do when using raw input as we can look for a TouchLocation.State value of Pressed. When it is detected, the object selection can be established ready for the objects to be dragged.
If we are using gestures, though, we have a problem:
there is no gesture that is triggered when contact is first established
with the screen. The tap gesture fires only when contact is released,
and the drag gestures fire only once the touch point has moved far
enough to be considered as actually dragging. So how do we perform the
object selection?
The answer is to once again use raw input for this.
Raw input and gestures can be mixed together so that the initial screen
contact for object selection comes from raw input, and the dragging
comes from a gesture.
Once the objects are selected, we can update them in
response to the touch point moving around the screen. When using
gestures, we simply look for one of the drag gestures and read out the Delta property of the GestureSample object. This contains the distance that the touch point has moved on each axis, which is exactly what we need.
Don't forget that the HorizontalDrag and VerticalDrag
gestures will provide only delta values for the appropriate axis. There
is no need to cancel out or ignore the other movement axis because XNA
takes care of this automatically.
|
|
To calculate the delta using raw input, we obtain the previous touch position using the TryGetPreviousLocation
function and subtract that position from the current position. The
result is the movement distance. The code for this is shown in Listing 13.
Example 13. Calculating the drag delta when using raw touch input
if (touches[0].State == TouchLocationState.Moved)
{
// Drag the objects. Make sure we have a previous position
TouchLocation previousPosition;
if (touches[0].TryGetPreviousLocation(out previousPosition))
{
// Calculate the movement delta
Vector2 delta = touches[0].Position - previousPosition.Position;
ProcessDrag(delta);
}
}
|
Whichever method we used to calculate the delta, we
now simply add the delta value to the position of all the selected
sprites. They will then follow the touch location as it is moved around
the screen.
Two example projects are provided to demonstrate this: DragAndFlick is a gesture-based implementation, whereas DragAndFlickRaw achieves the same effect using raw touch data. Both projects contain a SelectableSpriteObject class based on the one from the HitTesting project and contain identical functions for selecting the sprites at a point (SelectAllMatches), deselecting the sprites (DeselectAllObjects), and dragging the selected objects (ProcessDrag). There are some additional properties present, but we will look at them in the next section.
Try running both of the examples and see how they
work. You'll notice that they don't feel exactly the same, even though
they do essentially the same thing. The gesture-based project has a
delay between when you move the touch point and when the objects
actually respond to the movement. The reason for this is that the
gesture system waits for the touch point to move a small distance before
it considers a drag gesture to have started. As a result, it feels a
little less responsive.
The raw touch input assumes that all movement is part
of a drag, so there is no delay at all. As a result, it feels a lot
more responsive. Bear this difference in mind when considering the input
options that are available when you are coding your games.
4.2. Flicking Objects
With the object movement under our control, it is sometimes useful to allow the user to flick or throw them across the screen. This is often known as kinetic
movement, and it consists of retaining the velocity at which the object
is moving when the touch point is released and continuing to move the
object in the same direction, gradually decreasing the speed to simulate
friction.
To control the movement of the object, some new code has been added to the SelectableSpriteObject class. This code consists of a new Vector2 property called KineticVelocity, which tracks the direction and speed of movement; a float property called KineticFriction, which controls how strong the friction effect is (as a value between 0 and 1), and an Update override that applies the movement and the friction.
The Update code simply adds the velocity to
the position, and then multiplies the velocity by the friction value.
This function is shown in Listing 14. Notice how it uses the MathHelper.Clamp
function to ensure that the friction is always kept between 0 and 1
(values outside of this range would cause the object to accelerate,
which is probably undesirable, though perhaps it might be useful in one
of your games!).
Example 14. Updating the SelectableSpriteObject to allow it to observe kinetic movement
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
// Is the movement vector non-zero?
if (KineticVelocity != Vector2.Zero)
{
// Yes, so add the vector to the position
Position += KineticVelocity;
// Ensure that the friction value is within range
KineticFriction = MathHelper.Clamp(KineticFriction, 0, 1);
// Apply 'friction' to the vector so that movement slows and stops
KineticVelocity *= KineticFriction;
}
}
|
With the help of this code, the objects can respond
to being flicked, so now we need to establish how to provide them with
an initial KineticVelocity in response to the user flicking them. The example projects both contain a function called ProcessFlick, which accepts a delta vector as a parameter and provides it to all the selected objects.
To calculate this flick delta using the gesture input system is very easy. We have already looked at the Flick gesture and seen how to translate its pixels-per-second Delta value into pixels-per-update. We can do this now and provide the resulting Vector2 value to the ProcessFlick function, as shown in Listing 15.
Example 15. Initiating object flicking using gesture inputs
while (TouchPanel.IsGestureAvailable)
{
GestureSample gesture = TouchPanel.ReadGesture();
switch (gesture.GestureType)
{
case GestureType.Flick:
// The object has been flicked
ProcessFlick(gesture.Delta * (float)TargetElapsedTime.TotalSeconds);
break;
[... handle other gestures here...]
}
}
|
Unfortunately, using raw input is a little more work.
If we calculate the delta of just the final movement, we end up with a
fairly unpredictable delta value because people tend to involuntarily
alter their finger movement speed as they release contact with the
screen. This is coupled with the fact that the Released state always reports the same position as the final Moved state, meaning that it alone doesn't provide us with any delta information at all.
To more accurately monitor the movement delta, we
will build an array containing a small number of delta vectors (five is
sufficient), and will add to the end of this array each time we process a
Moved touch state. At the point of touch release, we can then
calculate the average across the whole array and use it as our final
movement delta.
This is implemented using three functions: ClearMovementQueue, AddDeltaToMovementQueue, and GetAverageMovementDelta. The first of these clears the array by setting all its elements to have coordinates of float.MinValue. We can look for this value when later processing the array and ignore any elements that have not been updated. ClearMovementQueue is called each time a new touch point is established with the screen.
AddDeltaToMovementQueue shifts all existing array elements down by one position and adds the provided delta to the end, as shown in Listing 16. This ensures that we always have the most recent delta values contained within the array, with older values being discarded. AddDeltaToMovementQueue is called each time we receive a touch point update with a state of Moved, with the delta vector calculated as described in the previous section.
Example 16. Adding new delta values to the movement queue
private void AddDeltaToMovementQueue(Vector2 delta)
{
// Move everything one place up the queue
for (int i = 0; i < _movementQueue.Length - 1; i++)
{
_movementQueue[i] = _movementQueue[i + 1];
}
// Add the new delta value to the end
_movementQueue[_movementQueue.Length - 1] = delta;
}
|
Finally, the GetAverageMovementDelta calculates the average of the values stored within the array, as shown in Listing 17. Any items whose values are still set to float.MinValue are ignored. The returned vector is ready to be passed into the ProcessFlick
function. Of course, the movement array is storing deltas in
distance-per-update format, so we have no need to divide by the update
interval as we did for gestures. GetAverageMovementDelta is called (along with ProcessFlick) when a touch point is detected with a state of Released.
Example 17. Calculating the average of the last five delta values
private Vector2 GetAverageMovementDelta()
{
Vector2 totalDelta = Vector2.Zero;
int totalDeltaPoints = 0;
for (int i = 0; i < _movementQueue.Length; i++)
{
// Is there something in the queue at this index?
if (_movementQueue[i].X > float.MinValue)
{
// Add to the totalMovement
totalDelta += _movementQueue[i];
// Increment to the number of points added
totalDeltaPoints += 1;
}
}
// Divide the accumulated vector by the number of elements
// to retrieve the average
return (totalDelta / totalDeltaPoints);
}
|
The main Update loop for the raw input example is shown in Listing 18.
You will see here the situations that cause it to deselect objects,
select objects, and reset the movement queue(when a new touch point is
made), drag the objects and add their deltas to the movement queue (when
an existing touch point is moved), and calculate the average and
process the object flick (when a touch point is released).
Example 18. The update code for selecting, dragging, and flicking objects using raw touch data
// Get the raw touch input
TouchCollection touches = TouchPanel.GetState();
// Is there a touch?
if (touches.Count > 0)
{
// What is the state of the first touch point?
switch (touches[0].State)
{
case TouchLocationState.Pressed:
// New touch so select the objects at this position.
// First clear all existing selections
DeselectAllObjects();
// The select all touched sprites
SelectAllMatches(touches[0].Position);
// Clear the movement queue
ClearMovementQueue();
break;
case TouchLocationState.Moved:
// Drag the objects. Make sure we have a previous position
TouchLocation previousPosition;
if (touches[0].TryGetPreviousLocation(out previousPosition))
{
// Calculate the movement delta
Vector2 delta = touches[0].Position - previousPosition.Position;
ProcessDrag(delta);
// Add the delta to the movement queue
AddDeltaToMovementQueue(delta);
}
break;
case TouchLocationState.Released:
// Flick the objects by the average queue delta
ProcessFlick(GetAverageMovementDelta());
break;
}
}
|
Try flicking the objects in each of the two DragAndFlick
projects. The behavior of this operation is much more consistent
between the two than it was for dragging. Also try experimenting with
different friction values and see how this affects the motion of the
objects when they are flicked.
5. Finger-Friendly Gaming
When designing the input mechanisms for your game,
always be aware that people will use their fingers to control things.
Unlike stylus input that was commonly used on earlier generations of
mobile devices, fingers are inherently inaccurate when it comes to
selecting from small areas on the screen.
With a little planning, you can help the user to have
a comfortable experience despite this limitation; without any planning,
you can turn your game into an exercise in frustration! If you have
lots of objects that can be selected in a small area, give some thought
to how you can help the user to select the object they actually desire
rather than having them continually miss their target.
One option is to allow users to hold their finger on
the screen and slide around to select an object rather than simply
tapping an object. As they slide their finger, a representation of the
selected object can be displayed nearby to highlight the current
selection (which, of course, will be obscured by the finger). Once users
have reached the correct place, they can release contact, happy that
they have picked the object they desired.
Another possibility is to magnify the area of the
screen surrounding the touch point, making all the objects appear
larger. Users can then easily select the object they want, at which
point the magnified area disappears.
Finger-friendly input options don't need to involve a
lot of additional work, especially if they are planned and implemented
early in a game's development, and it is definitely a good idea to avoid
putting off your target audience with fiddly and unpredictable input
mechanisms wherever possible.